﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace 读取西门子PLC_Demo
{
    public class SiemensClient
    {
        /// <summary>
        /// 第一次初始化指令交互报文
        /// </summary>
        private static readonly byte[] Command1 = new byte[22]
        {
            0x03,0x00,0x00,0x16,0x11,0xE0,0x00,0x00,
            0x00,0x01,0x00,0xC0,0x01,0x0A,0xC1,0x02,
            0x01,0x02,0xC2,0x02,0x01,0x00
        };

        /// <summary>
        /// 第二次初始化指令交互报文
        /// </summary>
        private static readonly byte[] Command2 = new byte[25]
        {
            0x03,0x00,0x00,0x19,0x02,0xF0,0x80,0x32,
            0x01,0x00,0x00,0x04,0x00,0x00,0x08,0x00,
            0x00,0xF0,0x00,0x00,0x01,0x00,0x01,0x01,0xE0
        };

        /// <summary>
        /// Socket实例
        /// </summary>
        protected Socket socket;

        /// <summary>
        /// ip地址
        /// </summary>
        public string IpAddress { get; set; }

        /// <summary>
        ///端口号
        /// </summary>
        public int Port { get; set; }

        /// <summary>
        /// 连接状态
        /// </summary>
        public bool Connected { get; set; } = false;

        public int BufferSize { get; set; } = 4096;

        /// <summary>
        /// 连接设备
        /// </summary>
        /// <returns></returns>
        public Result Connect()
        {
            var result = new Result();
            Disconnect();
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                //超时时间设置,临时写死
                socket.ReceiveTimeout = 2000;
                socket.SendTimeout = 2000;
                //连接 tcp连接
                var IP = IPAddress.Parse(IpAddress);
                socket.Connect(new IPEndPoint(IP, Port));
                //第一报文确认
                socket.Send(Command1);
                var socketReadResul = socket.Read(4);
                if (!socketReadResul.IsSucceed) return socketReadResul;
                var head1 = socketReadResul.Value;
                var length = head1[2] * 256 + head1[3] - 4;
                socketReadResul = socket.Read(length);
                if (!socketReadResul.IsSucceed) return socketReadResul;
                //第二次报文确认
                socket.Send(Command2);
                socketReadResul = socket.Read(4);
                if (!socketReadResul.IsSucceed)
                    return socketReadResul;
                var head2 = socketReadResul.Value;
                var length2 = head2[2] * 256 + head2[3] - 4;
                socketReadResul = socket.Read(length2);
                if (!socketReadResul.IsSucceed) return socketReadResul;
            }
            catch (Exception ex)
            {
                socket?.SafeClose();
                result.IsSucceed = false;
                result.Err = ex.Message;
            }
            return result;
        }

        /// <summary>
        /// 断开连接
        /// </summary>
        /// <returns></returns>
        public Result Disconnect()
        {
            Result result = new Result();
            try
            {
                if (socket?.Connected ?? false) socket?.Shutdown(SocketShutdown.Both);//正常关闭连接
            }
            catch (Exception e)
            {
                result.IsSucceed = false;
                result.Exception = e;
                return result;
            }
            try
            {
                socket?.Close();
            }
            catch (Exception e)
            {
                result.IsSucceed = false;
                result.Exception = e;
                return result;
            }
            return result;
        }

        /// <summary>
        /// 读取字节的方法
        /// </summary>
        /// <param name="store"></param>
        /// <param name="DB"></param>
        /// <param name="StartByteAdr"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public byte[] ReadBytes(SiemensStore store, int DB, int StartByteAdr, int count)
        {
            //获取读取命令
            var readcommand = GetReadCommand(store, DB, StartByteAdr, count);
            //发送读取命令并读取返回值(字节数组的形式)
            return SendAndRevice(readcommand).ResponseData;
        }

        /// <summary>
        /// 根据地址和数据类型,获取解析后的值
        /// </summary>
        /// <param name="address"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public object Read(string address, DataTypeEnum type)
        {
            //解析地址
            var varable = AddressAnalysis(address);
            var bytes = ReadBytes(varable.SiemensStore, varable.DBNum, varable.StartByteAddress, MethodExt.GetBytelength(type));
            switch (type)
            {
                case DataTypeEnum.None:
                    break;

                case DataTypeEnum.Bool:
                    return bytes.GetBit(varable.BitOffset);

                case DataTypeEnum.Byte:
                    break;

                case DataTypeEnum.Int16:
                    return BitConverter.ToInt16(bytes.Format(EndianFormat.DCBA), 0);

                case DataTypeEnum.UInt16:
                    return BitConverter.ToUInt16(bytes.Format(EndianFormat.DCBA), 0);

                case DataTypeEnum.Int32:
                    return BitConverter.ToInt32(bytes.Format(EndianFormat.DCBA), 0);

                case DataTypeEnum.UInt32:
                    break;

                case DataTypeEnum.Int64:
                    break;

                case DataTypeEnum.UInt64:
                    break;

                case DataTypeEnum.Float:

                    return BitConverter.ToSingle(bytes.Format(EndianFormat.DCBA), 0);

                case DataTypeEnum.Double:
                    break;

                case DataTypeEnum.String:
                    break;

                case DataTypeEnum.Wstring:
                    break;

                case DataTypeEnum.Custom:
                    break;

                default:
                    break;
            }

            return null;
        }

        protected static readonly object Readlock = new object();

        /// <summary>
        /// 发送报文并读取报文
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        protected Result<byte[]> SendAndRevice(byte[] command)
        {
            int GetContentLength(byte[] head)
            {
                if (head?.Length >= 4)
                    return head[2] * 256 + head[3] - 4;
                else
                    throw new ArgumentException("SendAndRevice返回报文头错误");
            }
            //从发送命令到读取响应为最小单元，避免多线程执行串数据（可线程安全执行）
            lock (Readlock)
            {
                Result<byte[]> result = new Result<byte[]>();
                try
                {
                    socket.Send(command);
                    var socketReadResul = socket.Read(4);
                    if (!socketReadResul.IsSucceed)
                        return socketReadResul;
                    var headPackage = socketReadResul.Value;

                    socketReadResul = socket.Read(GetContentLength(headPackage));
                    if (!socketReadResul.IsSucceed)
                        return socketReadResul;
                    var dataPackage = socketReadResul.Value;

                    result.Value = headPackage.Concat(dataPackage).ToArray();
                    var lbytes = new byte[2] { result.Value[3], result.Value[2] };
                    var length = BitConverter.ToUInt16(lbytes, 0);
                    if (length >= 25)
                    {
                        result.ResponseData = new byte[length - 25];
                        Array.Copy(result.Value, 25, result.ResponseData, 0, length - 25);
                    }
                    return result;
                }
                catch (Exception ex)
                {
                    result.IsSucceed = false;
                    result.Err = ex.Message;
                    return result;
                }
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="store"></param>
        /// <param name="DB"></param>
        /// <param name="StartByteAdr"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        protected byte[] GetReadCommand(SiemensStore store, int DB, int StartByteAdr, int count)
        {
            byte[] command = new byte[31];
            command[0] = 0x03;
            command[1] = 0x00;

            command[2] = 0x00;// (byte)(command.Length / 256);
            command[3] = 0x1F;// (byte)(command.Length % 256);//[2][3]为整个读取请求长度为0x1F= 31个字节   备注:优化可以一下读取多个区
            command[4] = 0x02;
            command[5] = 0xF0;
            command[6] = 0x80;//COTP
            command[7] = 0x32;//协议ID
            command[8] = 0x01;
            command[9] = 0x00;
            command[10] = 0x00;

            command[11] = 0x00;
            command[12] = 0x01;

            command[13] = 0x00;//
            command[14] = 0x0E;// 0E=14;他其实真实含义是报文中参数的字节个数,也就是从command[16]之后;因为command[17-30]的个数为14,所以这里是14;因为涉及同时读取多个存储区的话,可以在后面继续跟,我们这儿始终一次报文只读一个区,所以,固定14

            command[15] = 0x00;
            command[16] = 0x00;//读取的时候,固定这个就好,写入的时候,这里是写入的字节个数+4

            command[17] = 0x04;//04表示读 05表示写,所以这里也固定了
            command[18] = 0x01;// (byte)group.Length;//读取存储区个数,我们一次只读一个,所以也固定了

            command[19] = 0x12;//固定 variable specification
            command[20] = 0x0A;//固定 Length of following address specification
            command[21] = 0x10;//固定 Syntax Id: S7ANY
            command[22] = 0x02;//固定为02;解释:01表示按照bit位一个一个读取;02表示按照字节一个一个读取;这里没有怎么理解到,资料各种解释都有,一般都是02就对了
            command[23] = (byte)(count / 256);
            command[24] = (byte)(count % 256);

            command[25] = (byte)(DB / 256);
            command[26] = (byte)(DB % 256);//DB块的编号,如果不是DB区,这个参数可以随意设置,一般设置0就好

            command[27] = (byte)store;//访问数据块的类型
            command[28] = (byte)(StartByteAdr * 8 / 256 / 256 % 256);
            command[29] = (byte)(StartByteAdr * 8 / 256 % 256);
            command[30] = (byte)(StartByteAdr * 8 % 256);//[28][29][30]访问DB块的偏移量,这里是按照bit位置来计算的,去掉西门子的优化块访问就能看到这个了
            return command;
        }

        /// <summary>
        /// 地址解析
        /// </summary>
        /// <returns></returns>
        private VarableSiemens AddressAnalysis(string Address)
        {
            int GetBeingAddress(string address)
            {
                if (address.IndexOf('.') < 0)
                    return int.Parse(address);
                else
                {
                    string[] temp = address.Split('.');
                    return Convert.ToInt32(temp[0]);
                }
            }
            int GetBitOffset(string address)
            {
                if (address.IndexOf('.') < 0)
                    return 0;
                else
                {
                    string[] temp = address.Split('.');
                    return Convert.ToInt32(temp[1]);
                }
            }
            Result result = new Result();
            VarableSiemens var = new VarableSiemens();
            try
            {
                Address = Address.Norm();
                switch (Address[0])
                {
                    case 'I':
                        var.SiemensStore = SiemensStore.I;
                        break;

                    case 'Q':
                        var.SiemensStore = SiemensStore.Q;
                        break;

                    case 'M':
                        var.SiemensStore = SiemensStore.M;
                        break;

                    case 'D':
                        var.SiemensStore = SiemensStore.DB;
                        string[] adds = Address.Split('.');
                        if (Address[1] == 'B')
                            var.DBNum = Convert.ToUInt16(adds[0].Substring(2));
                        else
                            var.DBNum = Convert.ToUInt16(adds[0].Substring(1));
                        break;

                    case 'T':
                        var.SiemensStore = SiemensStore.T;
                        break;

                    case 'C':
                        var.SiemensStore = SiemensStore.C;
                        break;

                    case 'V':
                        var.SiemensStore = SiemensStore.DB;
                        var.DBNum = 1;
                        break;
                }
                //DB块
                if (Address[0] == 'D' && Address[1] == 'B')
                {
                    //DB1.0.0、DB1.4（非PLC地址）
                    var indexOfpoint = Address.IndexOf('.') + 1;
                    if (Address[indexOfpoint] >= '0' && Address[indexOfpoint] <= '9')//char的比较
                    {
                        var.StartByteAddress = GetBeingAddress(Address.Substring(indexOfpoint));
                        var.BitOffset = GetBitOffset(Address.Substring(indexOfpoint));
                    }
                    //DB1.DBX0.0、DB1.DBD4（标准PLC地址）
                    else
                    {
                        var.StartByteAddress = GetBeingAddress(Address.Substring(Address.IndexOf('.') + 4));
                        var.BitOffset = GetBitOffset(Address.Substring(Address.IndexOf('.') + 4));
                    }
                }
                //非DB块
                else
                {
                    //I0.0、V1004的情况（非PLC地址）
                    if (Address[1] >= '0' && Address[1] <= '9')
                    {
                        var.StartByteAddress = GetBeingAddress(Address.Substring(1));
                        var.BitOffset = GetBitOffset(Address.Substring(1));
                    }
                    //VB1004的情况（标准PLC地址）
                    else
                    {
                        var.StartByteAddress = GetBeingAddress(Address.Substring(2));
                        var.BitOffset = GetBitOffset(Address.Substring(2));
                    }
                }
            }
            catch (Exception)
            {
                return null;
            }
            return var;
        }
    }
}